package pl.ps.wi.monitoring.station.services;


import java.awt.Toolkit;
import java.util.ArrayList;
import java.util.List;
import java.util.TimerTask;
import java.util.Scanner;
import java.io.BufferedReader;

import javax.smartcardio.Card;
import javax.smartcardio.CardChannel;
import javax.smartcardio.CardException;
import javax.smartcardio.CardTerminal;
import javax.smartcardio.CardTerminals;
import javax.smartcardio.CommandAPDU;
import javax.smartcardio.ResponseAPDU;
import javax.smartcardio.TerminalFactory;

import pl.ps.wi.monitoring.station.interfaces.CardStatusListener;
import pl.ps.wi.monitoring.station.interfaces.LogginAttemptListener;
import pl.ps.wi.monitoring.station.interfaces.PinEnterListener;
import pl.ps.wi.monitoring.station.Phone;
/**
* @class CardService
* @brief Plik zawiera definicje klasy CardService. Klasa ta dziedziczy po klasie TimerTask.
*
* @date 2010.03.20
* @version 1.00.00
*
*  Obsługa czytnika kart oraz jego usług. Klasa pozwalająca na pobieranie danych, dekodowanie i odczytywanie danych, oraz obsługę zdarzeń takich jak
 * przyjście połączenia, sprawdzanie poprawności podanego pinu oraz uprawnień użytkownika.
 * Pola klasy: 
* -# CardService::SELECT_DF_SELS 
* -# CardService::SELECT_EF_ELS 
* -# CardService::SELECT_MF 
 * -# CardService::cardInsert 
 * -# CardService::keyboard 
 * -# CardService::loginListeners 
 * -# CardService::pesel 
 * -# CardService::run 
 * -# CardService::statusListeners 
 * -# CardService::thread 
 *
 */
public class CardService extends TimerTask {
		/** Tablica bajtów zawierająca instrukcje sterujące dla urządzenia czytnika kart.
         * Instrukcja wybiera pole danych 'MF' karty.
         */
	private static byte[] SELECT_MF = { (byte) 0x00, (byte) 0xA4, (byte) 0x00,
			(byte) 0x0C, (byte) 0x02, (byte) 0x3F, (byte) 0x00 };
        /**
         * Tablica bajtów zawierająca instrukcje sterujące dla urządzenia czytnika kart.
         * Instrukcja wybiera pole danych 'DF' karty oraz katalog SELS.
         */
	private static byte[] SELECT_DF_SELS = { (byte) 0x00, (byte) 0xA4,
			(byte) 0x04, (byte) 0x0C, (byte) 0x07, (byte) 0xD6, (byte) 0x16,
			(byte) 0x00, (byte) 0x00, (byte) 0x30, (byte) 0x01, (byte) 0x01 };
        /**
         * Tablica bajtów zawierająca instrukcje sterujące dla urządzenia czytnika kart.
         * Instrukcja wybiera pole danych 'EF' karty oraz plik 'ELS' z danymi osoby.
         */
	private static byte[] SELECT_EF_ELS = { (byte) 0x00, (byte) 0xA4,
			(byte) 0x02, (byte) 0x0C, (byte) 0x02, (byte) 0x00, (byte) 0x02 };
	/**
	 * String zawierający numer pesel użytkownika karty
	 */
	private volatile String pesel;
	/**
	 * pole logiczne run
	 * Pole służy do kontroli uruchamiania procesu czytnika kart.
	 */
	private boolean run = true;
	/**
	 * Pole typu Thread , będące wątkiem służącym do nasłuchiwania.
	 */
	private final Thread thread;
	/**
	 * pole logiczne cardU
	 * Pole służy do kontroli uruchamiania procesu czytnika kart.
	 */
	private boolean cardInsert = false;
        /**
         * Pole typu KeyboardService, zawierające referencje do obiektu sterującego klawiaturą
         */
	private KeyboardService keyboard;
        /**
         * Lista zawierająca obiekty typu CardStatusListener służące do obsługi zdarzeń czytnika(wyjęcie oraz włożenie karty)
         */
	private final List<CardStatusListener> statusListeners;
        /**
         * Lista zawierająca obiekty typu LogginAttempListener służące do obsługi próby zalogowania użytkownika(podanie pinu)
         */
	private final List<LogginAttemptListener> loginListeners;
	/**
         * Bezparametrowy konstruktor klasy CardService, tworzy obiekty oraz rozpoczyna prace wątku.
         */
	public CardService() {
		statusListeners = new ArrayList<CardStatusListener>();
		loginListeners = new ArrayList<LogginAttemptListener>();

		thread = new Thread(this, "card service");
		thread.start();
	}
	/**
	 * Metoda dodająca obiekt typu CardStatusListener do listy.
	 * @param listener
	 */
	public void addCardStatusListener(CardStatusListener listener) {
		statusListeners.add(listener);
	}
	/**
	 * Metoda usuwająca obiekt typu CardStatusListener z listy.
	 * @param listener
	 */
	public void removeCardStatusListener(CardStatusListener listener) {
		statusListeners.remove(listener);
	}
	/**
	 * Metoda dodająca obiekt typu LogginAttemptListener do listy.
	 * @param listener
	 */
	public void addLoggingAttemptListener(LogginAttemptListener listener) {
		loginListeners.add(listener);
	}
	/**
	 * Metoda usuwająca obiekt typu LogginAttemptListener z listy.
	 * @param listener
	 */
	public void removeLoggingAttemptListener(LogginAttemptListener listener) {
		loginListeners.remove(listener);
	}
        /**
         * Metoda wywoływana w momencie włożenia karty - uruchamia ona sekwencje komend APDU.
         * Wykonywana jest dopóki zmienna run == true.
         * Pobierane są dane z karty, sprawdzana jest zgodność numeru pesel znajdującego się na karcie oraz numeru pin. Wykorzystywana jest przeciążana wewnątrz metoda OnPinEnter(String pin)
		 * Jeżeli nie uda się połączyć z czytnikiem kart, najprawdopodobniej oznacza to odłączenie go od systemu. Wysyłana jest wtedy wiadomość (zdarzenie "annihilation"). Aby upewnić się, że sms zostanie wysłany tylko raz, ustawiana jest flaga alreadySent.
         */
	public void run() {
		CommandAPDU selecrMf = new CommandAPDU(SELECT_MF);
		CommandAPDU selectEfEls = new CommandAPDU(SELECT_EF_ELS);
		CommandAPDU selectDfSels = new CommandAPDU(SELECT_DF_SELS);

		TerminalFactory terminalFactory = TerminalFactory.getDefault();
		CardTerminals cardTerminals = terminalFactory.terminals();


		while (run) {
//			waitForCardReaderStatusChange(cardTerminals);
			List<CardTerminal> cardList;
			try {
				cardList = cardTerminals.list(CardTerminals.State.CARD_INSERTION);
			} catch (CardException e1) {
				//e1.printStackTrace();
				System.out.println("Sending message: system under attack");
				Phone.sendAnnihilationMessage();
				continue;
			}
			
			if (cardList.size() > 0) {
				fireCardInsert();
				cardInsert = true;
			}

			for (CardTerminal cardTerminal : cardList) {
				Card card = connectToTerminal(cardTerminal, "T=1");

				if (card == null) { // prevent from NullPointerExeption
					continue;
				}
				
				CardChannel cardChannel = card.getBasicChannel();

				if (check9000(cardChannel, selecrMf) &&check9000(cardChannel, selectDfSels)
						&& check9000(cardChannel, selectEfEls)) {
					CardData cardData = getDataFromCard(cardChannel);
					String[] dane_karty = decodeData(cardData);
					// TODO pesel jest pod zmienna dane_karty[6] ,wpisac do jakies zmiennej globalnej
					pesel = dane_karty[6];
					KeyboardService keyboard = new KeyboardService();
					keyboard.addPinEnterListener(new PinEnterListener() {
						@Override
						public void onPinEnter(String pin) {
							for (LogginAttemptListener listener : loginListeners) {
								listener.onLogginAttempt(pesel, pin);
							}
						}
					});
					if (pesel.matches("\\d{11}")) {
						//Toolkit.getDefaultToolkit().beep();						
						keyboard.listen();
						//Toolkit.getDefaultToolkit().beep();
					}
				} else {
					continue;
				}

			}

//			waitForCardReaderStatusChange(cardTerminals);
			try {
				List<CardTerminal> removedCardList = cardTerminals.list(CardTerminals.State.CARD_REMOVAL);
				if (removedCardList.size() > 0 && !cardInsert) {
					fireCardRemove();
				}
				for (CardTerminal cardTerminal : removedCardList) {
					if (keyboard != null) {
						keyboard.interupt();
					}
					System.out.println("removal test");
				}
			} catch (CardException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			waitForCardReaderStatusChange(cardTerminals);
			cardInsert = false;
		}
	}
	/**
         * Metoda wyłącza zmienną sterująca 'run' odpowiedzialną za obsługę zdarzeń oraz kończy wątek.
         */
	public void stopSerevice() {
		run = false;
		try { // wait for thread
			thread.join();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
        /**
         * Metoda wywołująca obsługę zdarzeń wyjęcia karty
         */
	private void fireCardRemove() {
		for (CardStatusListener listener : statusListeners) {
			listener.onCardRemove();
		}
	}
       /**
         * Metoda wywołująca obsługę włożenia karty
         */
	private void fireCardInsert() {
		for (CardStatusListener listener : statusListeners) {
			listener.onCardInset();
		}
	}
	/**
	 * Metoda ustanawiająca połączenie czytnika z kartą
	 * @param cardTerminal
      * @param string
      * @return Card
	 */
	private Card connectToTerminal(CardTerminal cardTerminal, String string) {
		try {
			return cardTerminal.connect("T=1");
		} catch (CardException e) {
			// TODO log exception
			e.printStackTrace();
			return null;
		}
	}
        /**
         * Metoda oczekuje na włożenie lub wyjęcie karty z jakiegokolwiek terminala
         * @param cardTerminals
         */
	private void waitForCardReaderStatusChange(CardTerminals cardTerminals) {
		try {
			cardTerminals.waitForChange();
		} catch (CardException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
/**
 * Metoda dekoduje dane pobrane wcześniej z karty i zwraca je w postaci tablicy stringów
 * @param cardData
 * @return dane_karty
 */
	private String[] decodeData(CardData cardData) {
		int size;
		String form_data;
		int nr_danych = 0;
		String[] dane_karty = new String[9];
		
		for (int i = 0; i < cardData.size(); i++) {
			String data = cardData.getData(i);
			if (data != null) {
				size = 4 + cardData.getDataLen(i);
				if (size > data.length()) {
					size = data.length();
				}
				form_data = data.substring(4, size);

				if ((i > 0) && (nr_danych < 9)) {
					if (i != 5) {
						dane_karty[nr_danych] = form_data;
						nr_danych++;
					} else if ((i == 5) && (cardData.isDataIsString(i))) {
						dane_karty[nr_danych - 1] += " " + form_data;
					} else {
						dane_karty[nr_danych] = form_data;
						nr_danych++;
					}
				}
			}
		}
		return dane_karty;
	}
/**
 * Metoda pobiera dane z karty i zwraca je w postaci obiektu CardData.
 * @param cardChannel
 * @return CardData
 */
	private CardData getDataFromCard(CardChannel cardChannel) {
		ResponseAPDU response;

		int offset = 0;
		int nr_stringu = 0;
		int[] result_len = new int[50];
		byte[] read_comm = new byte[5];
		String[] result = new String[50];
		boolean[] result_is_string = new boolean[50];

		for (int offs = 0; offs < 2; offs++) {
			read_comm[0] = (byte) 0x00;
			read_comm[1] = (byte) 0xB0;
			read_comm[2] = (byte) (((offset) >> 8) & 0xFF);
			read_comm[3] = (byte) ((offset) & 0xFF);
			read_comm[4] = (byte) 0xFF;

			CommandAPDU readCommand = new CommandAPDU(read_comm);
			try {
				response = cardChannel.transmit(readCommand);
			} catch (CardException e) {
				// TODO log exception
				e.printStackTrace();
				return null;
			}

			byte[] tmp_dane = response.getBytes();
			boolean last_byte_control = false, hex_0x0C = false;

			for (int i = 0; i < tmp_dane.length - 2; i++) {
				int code = (int) tmp_dane[i];
				if (code > 0) {
					char c = (char) tmp_dane[i];

					if ((code > 31) && (code < 256) && !hex_0x0C) {
						result[nr_stringu] += c;
					}

					if (hex_0x0C) {
						hex_0x0C = false;
						result_len[nr_stringu] = code;
					}

					if (((code == (int) 0x0C) || (code == (int) 0x13) || (code == (int) 0x18))
							&& (!last_byte_control)) {
						nr_stringu++;
						last_byte_control = true;
						if (code == (int) 0x0C) {
							result_is_string[nr_stringu] = true;
						} else {
							result_is_string[nr_stringu] = false;
						}
						hex_0x0C = true;
					} else {
						last_byte_control = false;
					}
				}
			}

			offset += 64;
		}
		return new CardData(result, result_len, result_is_string);
	}
/**
 * Metoda sprawdza czy odpwiedź metody na komendę APDU ma status 9000 i zwraca true jeżeli tak.
 * @param cardChannel
 * @param cmd
 * @return
 */
	private boolean check9000(CardChannel cardChannel, CommandAPDU cmd) {
		try {
			byte[] response = cardChannel.transmit(cmd).getBytes();
			return (response[response.length - 2] == (byte) 0x90 && response[response.length - 1] == (byte) 0x00);
		} catch (CardException e) {
			// TODO log exception
			e.printStackTrace();
			return false;
		}
	}

}
